package org.zhaolei.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Arrays;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.zhaolei.enums.BizCodeEnum;
import org.zhaolei.enums.CouponCategoryEnum;
import org.zhaolei.enums.CouponPublishEnum;
import org.zhaolei.enums.CouponStateEnum;
import org.zhaolei.exception.BizException;
import org.zhaolei.interceptor.LoginInterceptor;
import org.zhaolei.mapper.CouponMapper;
import org.zhaolei.mapper.CouponRecordMapper;
import org.zhaolei.model.CouponDO;
import org.zhaolei.model.CouponRecordDO;
import org.zhaolei.model.LoginUser;
import org.zhaolei.request.NewUserCouponRequest;
import org.zhaolei.service.CouponService;
import org.zhaolei.util.CommonUtil;
import org.zhaolei.util.JsonData;
import org.zhaolei.vo.CouponVO;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class CouponServiceImpl implements CouponService {

    @Autowired
    private CouponMapper couponMapper;

    @Autowired
    private CouponRecordMapper couponRecordMapper;

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public Map<String, Object> pageCouponActivity(int page, int size) {
        Page<CouponDO> pageInfo = new Page<>(page, size);

        IPage<CouponDO> couponDOIPage = couponMapper.selectPage(pageInfo, new QueryWrapper<CouponDO>()
                .eq("publish", CouponPublishEnum.PUBLISH)
                .eq("category", CouponCategoryEnum.PROMOTION)
                .orderByDesc("create_time"));

        Map<String, Object> pageMap = new HashMap<>(3);
        //总条数
        pageMap.put("total_record", couponDOIPage.getTotal());
        pageMap.put("total_page", couponDOIPage.getPages());
        pageMap.put("current_data", couponDOIPage.getRecords().stream().map(
//                obj->beanProcess(obj)
                this::beanProcess
        ).collect(Collectors.toList()));

        return pageMap;
    }

    /**
     * 领券接口
     * 1. 获取优惠券是否存在
     * 2. 校验优惠券是否可以领取：时间、库存、数量限制
     * 3. 扣减库存
     * 4. 保存领券记录
     * @param couponId
     * @param couponCategoryEnum
     * @return
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    @Override
    public JsonData addCoupon(long couponId, CouponCategoryEnum couponCategoryEnum) {
        //获取当前登录的用户
        LoginUser loginUser = LoginInterceptor.threadLocal.get();
        //用redis原生语言写的分布式锁比较冗余，业务代码只有一点点，加锁和解锁都很麻烦
//        String uuid = CommonUtil.generateUUID();
//        String lockKey = "lock:coupon:" + couponId;
//        ArrayList<String> keys = new ArrayList<>();
//        keys.add(lockKey);
//        //避免锁过期，一般配久一点
//        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, Duration.ofMinutes(5));
        String lockKey = "lock:coupon:" + couponId;
        RLock rLock = redissonClient.getLock(lockKey);
        //多个线程进入时，会等待释放锁，默认有效期30秒，会自动延期
        rLock.lock();
        log.info("领劵接口加锁成功:{}",Thread.currentThread().getId());
//        if (lockFlag) {
//            log.info("加锁成功：{}", couponId);
        try {
            //执行业务逻辑
            //检查优惠券是否存在
            CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>()
                    .eq("id", couponId)
                    .eq("category", couponCategoryEnum.name()));

            //检查优惠券是否可以领取
            this.checkCoupon(couponDO, loginUser.getId());

            //构建领券记录
            CouponRecordDO couponRecordDO = new CouponRecordDO();
            BeanUtils.copyProperties(couponDO, couponRecordDO);
            couponRecordDO.setCreateTime(new Date());
            couponRecordDO.setUseState(CouponStateEnum.NEW.name());
            couponRecordDO.setUserId(loginUser.getId());
            couponRecordDO.setUserName(loginUser.getName());
            couponRecordDO.setCouponId(couponDO.getId());
            //使用数据表里自增的id
            couponRecordDO.setId(null);

            //扣减库存，有漏洞版本
            int rows = couponMapper.reduceStock(couponId);

            //int flag = 1/0; 加入这个异常可以模拟库存已经减了，但是coupon record没有增加记录，这种情况就需要增加事务

            if (rows == 1) {
                couponRecordMapper.insert(couponRecordDO);
            } else {
                log.warn("发放优惠券失败：{},用户:{}", couponDO, loginUser);
                throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
            }
        } finally {
            //解锁，使用Lua脚本，确保操作的原子性，也满足幂等性
//            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//            Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), keys, uuid);
//            log.info("解锁：{}", result);
            rLock.unlock();
            log.info("解锁成功");
        }
//        } else {
//            //加锁失败，进入自旋
//            try {
//                TimeUnit.SECONDS.sleep(1);
//            } catch (InterruptedException e) {
//                log.error("自旋失败！！");
//            }
//            addCoupon(couponId, couponCategoryEnum);
//        }

        return JsonData.buildSuccess();
    }

    /**
     * 初始化新用户优惠券
     * 用户刚注册之后还没有登录，没有token，所以threadlocal里没有用户信息
     * @param newUserCouponRequest
     * @return
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    @Override
    public JsonData initNewUserCoupon(NewUserCouponRequest newUserCouponRequest) {
        //构造用户并加入threadLocal
        LoginUser loginUser = new LoginUser();
        loginUser.setId(newUserCouponRequest.getUserId());
        loginUser.setName(newUserCouponRequest.getName());
        LoginInterceptor.threadLocal.set(loginUser);

        //查询新用户可以发哪些优惠券
        List<CouponDO> couponDOList = couponMapper.selectList(new QueryWrapper<CouponDO>()
                .eq("category", CouponCategoryEnum.NEW_USER.name()));

        for (CouponDO couponDO : couponDOList) {
            //幂等操作，调用需要加锁
            this.addCoupon(couponDO.getId(), CouponCategoryEnum.NEW_USER);
        }
        return JsonData.buildSuccess();
    }

    /**
     * 把优惠券检验逻辑都下沉到这个函数里
     * @param couponDO
     * @param userId
     */
    private void checkCoupon(CouponDO couponDO, Long userId) {
        if (couponDO == null) {
            throw new BizException(BizCodeEnum.COUPON_NO_EXITS);
        }

        //判断库存是否足够
        if (couponDO.getStock() <= 0) {
            throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
        }
        //判断发布状态
        if (!couponDO.getPublish().equals(CouponPublishEnum.PUBLISH.name())) {
            throw new BizException(BizCodeEnum.COUPON_GET_FAIL);
        }
        //判断是否过期
        long time = CommonUtil.getCurrentTimestamp();
        long start = couponDO.getStartTime().getTime();
        long end = couponDO.getEndTime().getTime();
        if (time < start || time > end) {
            throw new BizException(BizCodeEnum.COUPON_OUT_OF_TIME);
        }

        //判断是否达到用户领取限制，获取用户已领取的优惠券数量，与优惠券自身的限制数量进行比较
        int recordNum = couponRecordMapper.selectCount(new QueryWrapper<CouponRecordDO>()
                .eq("user_id", userId)
                .eq("coupon_id", couponDO.getId()));

        if (recordNum >= couponDO.getUserLimit()) {
            throw new BizException(BizCodeEnum.COUPON_OUT_OF_LIMIT);
        }
    }

    private CouponVO beanProcess(CouponDO couponDO) {
        CouponVO couponVO = new CouponVO();
        BeanUtils.copyProperties(couponDO, couponVO);
        return couponVO;
    }
}
